using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Web;

namespace UDS.Web
{
    /// <summary>
    /// Transfers a file to a web client.
    /// </summary>
    public class HttpFileResponse : IDisposable
    {
        /// <summary>
        /// Initializes an instance of <see cref="HttpFileResponse"/> class.
        /// </summary>
        /// <param name="context">
        /// An <see cref="HttpContext"/> object that contains the detailed 
        /// information about the current HTTP request.
        /// </param>
        /// <param name="path">
        /// The path to the file to be sent.
        /// </param>
        public HttpFileResponse(HttpContext context, string path)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }
            if (!File.Exists(path))
            {
                throw new FileNotFoundException(string.Concat("The specified file was not found: ", path, "."), path);
            }

            _httpContext = context;
            _path = path;
            _offset = GetOffsetFromRequestHeader();
        }

        /// <summary>
        /// Releases all the resources.
        /// </summary>
        void IDisposable.Dispose()
        {
            Close();
        }

        /// <summary>
        /// When overridden by a descendant class, closes the response stream.
        /// </summary>
        public virtual void Close()
        {
            if (!_closed)
            {
                _closed = true;

                _httpContext.Response.End();
                _httpContext = null;
            }
        }

        /// <summary>
        /// Sends the requested file to the output stream.
        /// </summary>
        public void ProcessRequest()
        {
            CheckDisposed();

            FileStream stream;
            HttpResponse response;

            // Opens the specified file.
            using (stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                // Gets the size of the specified file
                int fileSize = (int)stream.Length;
                if (_offset >= fileSize)
                {
                    return;
                }

                // Writes the repsonse header fields first
                response = _httpContext.Response;
                WriteHeaders(response, fileSize);

                // Writes the file content then
                if (_offset > 0)
                {
                    // Advances the file pointer
                    stream.Seek(_offset, SeekOrigin.Begin);
                }

                int currentPos = _offset;
                const int BufferSize = 1024 * 64; // 64K

                using (BinaryReader reader = new BinaryReader(stream))
                {
                    while (response.IsClientConnected && currentPos < fileSize)
                    {
                        byte[] buffer = reader.ReadBytes(BufferSize);

                        if (buffer != null && buffer.Length > 0)
                        {
                            currentPos += buffer.Length;
                            try
                            {
                                response.OutputStream.Write(buffer, 0, buffer.Length);
                            }
                            catch (WebException)
                            {
                                // Ignores web exceptions
                                break;
                            }
                        }
                    }
                }
            }
        }

        private int GetOffsetFromRequestHeader()
        {
            // The "Range" request-header field contains the offset information. It looks like:
            //   Range=1024-
            string range = _httpContext.Request.Headers["Range"];
            if (!string.IsNullOrEmpty(range))
            {
                string[] array;

                array = range.Split(new char[] { '=', '-' }, StringSplitOptions.RemoveEmptyEntries);
                if (array != null && array.Length >= 2)
                {
                    int offset;
                    if (int.TryParse(array[1], out offset) && offset > 0)
                    {
                        return offset;
                    }
                }
            }

            return 0;
        }

        private void WriteHeaders(HttpResponse response, int totalLength)
        {
            // Clears all output content.
            response.Clear();
            response.ContentEncoding = System.Text.Encoding.UTF8;
            response.HeaderEncoding = System.Text.Encoding.UTF8;
            response.AddHeader("Content-Length", (totalLength - _offset).ToString());
            response.AddHeader("Accept-Ranges", "bytes");
            response.ContentType = "application/octet-stream";
            response.AddHeader("Content-Disposition", "attachment;filename=" + EncodedOutputName);
            if (_offset > 0)
            {
                response.AddHeader(
                  "Content-Range",
                  string.Format("bytes {0}-{1}/{2}", _offset, totalLength - 1, totalLength));
                response.StatusCode = (int)HttpStatusCode.PartialContent;
            }

            // Disable buffer output
            response.BufferOutput = false;
        }

        private void CheckDisposed()
        {
            if (_closed)
            {
                throw new ObjectDisposedException(GetType().FullName);
            }
        }

        /// <summary>
        /// Gets the path of the file whose content is sent to a client user.
        /// </summary>
        public string FilePath
        {
            get { return _path; }
        }

        /// <summary>
        /// Gets or sets the default output name 
        /// </summary>
        public string OutputName
        {
            get
            {
                CheckDisposed();
                return _outputName;
            }
            set
            {
                CheckDisposed();
                _outputName = value;
            }
        }

        /// <summary>
        /// Gets the starting position in the specified file to begin writing.
        /// </summary>
        public int Offset
        {
            get
            {
                CheckDisposed();
                return _offset;
            }
        }

        /// <summary>
        /// Gets or sets the number of miniseconds for which the transferring 
        /// thread was blocked, after a buffer(64K by default) was sent to a client 
        /// user. Specify zero (0) to disable the transferring speed tune.
        /// </summary>
        public int SleepDuration
        {
            get { return _sleepDuration; }
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException("value");
                }
                _sleepDuration = value;
            }
        }

        private string EncodedOutputName
        {
            get
            {
                string name = _outputName;

                // Checks if the OutputName property was set?
                if (string.IsNullOrEmpty(name))
                {
                    name = Path.GetFileName(_path);
                }

                return HttpUtility.UrlEncode(name);
            }
        }

        private int _sleepDuration = 50;
        private bool _closed;
        private string _path;
        private string _outputName;
        private int _offset;
        private HttpContext _httpContext;
    }
}